查看原文
其他

一个表情引发的思考

石文帅 京东技术 2019-05-31

来这里找志同道合的小伙伴!

 简 介 

字符集的由来是什么?各种字符编码又有什么关系?乱码是如何出现的?带着这些问题,大家一起倾听字符的故事。


前几天测试提了一个 bug,“在长度限定的文本区域,输入表情时会显示乱码”。由此作者产生了一些疑问:这些表情是什么东西?为什么会出现乱码? JS 是使用哪种编码方式?在查阅了相关文献后,最终找到了答案,今天就详细说一说编码的故事。


比特、字符、字节


聊编码之前,有几个基础的概念需要先明确一下:

  • 比特位:即 Bit,是计算机最小的存储单位。以 0 或 1 来表示比特位的值,也是网络信息传输的基本单位

  • 字节:8 个比特位表示一个字节

  • 字符:可使用多种不同字符方案或代码页来表示的抽象实体


大家知道,在计算机内部,所有的信息最终都表示为一个二进制的序列。每一个二进制位(bit)有 0 和 1 两种状态,因此八个二进制位就可以组合出 256 种状态,这被称为一个字节( byte )。也就是说,一个字节一共可以用来表示 256 种不同的状态或者符号。如果制作一张对应表格,对于每一个 8 位二进制序列,都对应唯一的一个符号。每一个状态对应一个符号,就是 256 个符号,从 00000000 到 11111111 。


东方的故事


上个世纪 60 年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定,这被称为 ASCII 码,一直沿用至今。但是汉字没办法表示,所以中国人民融合了 ASCII 中的数字、标点、字母,再把数字符号、罗马希腊字母与 7000 多个简体汉字整合进去,产生了 GB2312GB2312 是对 ASCII 的中文拓展。后来发现中华文化实在是博大精深,汉字太多,原来的编码不太够用,因此又做拓展,此拓展方案叫做 GBKGBK 中融合了 GB2312 的所有内容,同时新增 20000 个新的汉字(包括繁体字)和符号。再后来融合了少数民族的文字,GB18030便由此诞生。


统一字符集


1、规则制定者


全世界很多国家都有自己的文字编码,每个国家的编码还都不一样。不由会让人产生一个想法“为何不建立一个全世界统一的字符集呢?”


20世纪80、90年代,有两个组织在做这个事情:


  • 国际标准化组织(ISO)

    国际标准化组织是一个独立的非政府组织,它是世界上最大的自愿性国际标准开发商,通过在各国之间提供共同标准来促进世界贸易,ISO 试图制定一份“通用字符集”( UCS )。

  • 统一码联盟(Unicode Consortium)

    它的主要目的是维护和发布Unicode标准,该标准是为了替换现有字符编码方案而开发的,现有字符编码方案在大小和范围上有限制,并且与多语言环境不兼容。Unicode在统一字符集方面的成功,导致了它在软件国际化和本地化方面的广泛应用


两个机构的参与者意识到,世界上并不需要两个不兼容的字符集,因此双方开始合作。从 Unicode 2.0 开始,Unicode 采用了与 ISO 10646-1 相同的字库和字码;ISO 也承诺,ISO 10646 将不会替超出 U+10FFFF(Unicode 编码以 U+ 开头) 的 UCS-4 编码赋值,以使得两者保持一致。后来两个项目仍独立存在,并独立地公布各自的标准。不过由于 Unicode- 这一名字比较好记,因而它的使用更为广泛。


2、码点与区面


字符集从 0 开始为每个符号制定一个编码,叫码点。最新的 Unicode 版本一共有 109449 个符号。这些符号分区定义,每个区称作一个面,一共有 17(2^5)个面,每个面可以存放 65536(2^16) 个字符,也就是说一共可以存 2^21 个字符。17 个面中有一个基本平面( BMP ),16个辅助平面( SMP )。


总而言之 Unicode 指的是字符的集合,而每个字符如何表示,需要编码方法,大家知道的编码方法有 UTF-32 、UTF-16 、UTF-8、UCS-2 等等,他们到底是什么?又有什么关系呢?


UTF-32 与 UCS-4



在 Unicode 与 UCS 合并之前已经产生了 UCS-4 编码方式。 UCS-4 还是使用 32 位( 4 字节)来表示每个 Unicode 编码,整个代码值表示所述代码空间范围介于 0 和 十六进制 7FFFFFFF 之间。但实际使用范围不超过 0x10FFFF,为了兼容 Unicode 标准产生了 UTF-32,他的码值与 UCS-4 一致,不过编码空间被限定在 0~0x10FFFF 之间,因此可以说 UTF-32 是 UCS-4 的子集


由上文可以看出 UCS-4 存有明显的问题:由于 UTF-32 使用 4个字节来描绘每个字符,对于同样的英文文本来说,它是 ASCII 码使用空间的四倍。因此在HTML5网页标准中就明确规定,不得使用 UTF-32 进行编码。


UTF-16 与 UCS-2


1、UTF-16


UTF-16 的编码方式介于 UTF-32 和 UTF-8 之间,同时结合了定长和变长两种编码的特点。上文中提到过 Unicode 编码点有 17 个平面,一个基本平面,16 个辅助平面。UTF-16 的编码规则很简单,基本平面的字符占用两个字节,辅助平面的字符占用四个字节。就是说 UTF-16 编码要么是两个字节(U+0000到U+FFFF),要么是4个字节(U+010000到U+10FFFF)。


2、辅助平面字符表示


问题来了,如果遇到两个字节,如何确定这两个字节是一个字符,还是这两个字节拼上后面的两个字节形成的一个字符呢? 其实是这样的,在基本平面内,从 U+D800 到 U+DFFF 是一个空段,不对应任何码点,这个空段用来映射辅助平面的字符。如何映射呢?首先辅助平面的字符一共是 16*2^16 个,也就是 2^20 个。一共 20 位,前 10 位跟后 10 位分开,前 10 位称为高位(H),后 10 位称为低位(L)。即一个辅助平面的字符,被拆成两个基本平面的字符表示。


3、Unicode 转 UTF-16


如何将 Unicode 码点转成 UTF-16 呢?


基本平面字符,直接将码点转为 16 进制:

  1. U+597D=>0x597D


辅助平面字符:

  1. H = Math.floor((c-0x10000) / 0x400)+0xD800

  2. L = (c - 0x10000) % 0x400 + 0xDC00


4、UCS-2


UCS-2 又是什么,跟 UTF-16 又有什么关系呢?


JavaScript 采用了 Unicode 字符集,但是只支持一种编码方式。JS 最先采用的编码既不是 UTF-16 也不是 UTF-32 或 UTF-8 ,而是 UCS-2 。UTF-16明确宣布是 UCS-2 的超集,UTF-16 中基本平面字符延用 UCS-2 编码。辅助平面字符定义了 4 个字节的表示方法,UCS-2 被整合进了 UTF-16 。并且由于 JavaScript 诞生的时候还没有 UTF-16 编码(UCS-2 于 1990 年公布,而 UTF-16 于 1996 年公布),因此 JS 最先采用了已经被淘汰的 UCS-2。


那么问题就显而易见了,JS 只能处理 UCS-2 编码,造成所有字符在这门语言中都是两个字节,如果是四个字节的字符,会被当做两个双字节的字符处理。


UTF-8


如果理解了前文中说的 UTF-32 和 UTF-16 两种编码方式,不难发现一个问题,对于英语国家来说一个字符完全可以单字节编码,使用以上的两种编码方式是对带宽的极大浪费,UTF-8 便由此而生。


UTF-8 的编码规则:

  • 对于 ASCII 码中的符号,使用单字节编码,其编码值与 ASCII 值相同。其中 ASCII 值的范围为 0~0x7F,所有编码的二进制值中第一位为 0(这个正好可以用来区分单字节编码和多字节编码)

  • 其它字符用多个字节来编码(假设用 N 个字节),多字节编码需满足,第一个字节的前 N 位都为 1,第 N+1 位为 0,后面 N-1 个字节的前两位都为 10,这 N 个字节中其余位全部用来存储 Unicode 中的码位值。


字节数UnicodeUTF-8编码
1000000-00007F0xxxxxxx
2000080-0007FF110xxxxx 10xxxxxx
3000800-00FFFF1110xxxx 10xxxxxx 10xxxxxx
4010000-10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx


最初的疑惑



说到此处,便可以对最初的疑惑做出解释:


  • 这些表情是什么东西?

    emoji 是通过 UCS-2(UTF-16)编码的字符,由于码点在辅助平面因此需要四个字节表示,即两个字符,这也说明为什么 emoji 表情的 length 值为 2 。

  • 为什么会出现乱码?

    emoji 表情符号,在 JS 中需要用两个字符表示。当在文本框限定长度时,假设长度限定为 15 ,在第 15 个字符输入一个表情(占两个字符),此时该表情会被输入区域自动截取一半,此时便无法正确显示啦。

  • JS 是使用哪种编码方式?

    JS 早期使用 UCS-2 编码,后来使用 UTF-16,UTF-16 是 UCS-2 的超集。

  • 如何校验辅助平面的字符呢?(如 emoji 表情)

    依据其特点,正则校验方式如下:

  1. var patt=/[\ud800-\udbff][\udc00-\udfff]/g;

  • 问题解决方案

    不再使用原生 textarea 或者 input 组件的 maxlength 属性进行长度校验,改为通过 JS 校验。当输入内容超过指定长度时,校验限定长度的位置跟下一个位置,是否刚好可以拼成一个 emoji 表情(校验正则如上),如果为表情,截取至表情之前即可。


总结


  • Unicode 是字符集,不是编码方式,UTF-8、UTF-16、UTF-32、UCS-2、UCS-4 为编码方式。

  • 字节编码的“派别”归类

  • UTF-8、UTF-16、UTF-32编码比较


拓展阅读


[1]维基百科-unicode:
https://en.wikipedia.org/wiki/Unicode

[2]维基百科-国际标准化组织:
https://en.wikipedia.org/wiki/InternationalOrganizationfor_Standardization(iso)

[3]维基百科-统一码联盟:
https://en.wikipedia.org/wiki/Unicode_Consortium

[4]维基百科-通用字符集(UCS):
https://en.wikipedia.org/wiki/UniversalCodedCharacter_Set

[5]维基百科-UCS-4:
https://hu.wikipedia.org/wiki/UTF-32/UCS-4

[6]维基百科-UTF-32:
https://en.wikipedia.org/wiki/UTF-32

[7]维基百科-UTF-16:
https://en.wikipedia.org/wiki/UTF-16

[8]维基百科-UTF-8:
https://en.wikipedia.org/wiki/UTF-8

[9]Unicode与JavaScript详解:
http://www.ruanyifeng.com/blog/2014/12/unicode.html

[10]移动前端手机输入法自带emoji表情字符处理:
https://blog.csdn.net/binjly/article/details/47321043

[11]emoji存库以及乱码问题解决:
https://blog.csdn.net/Mr_LXming/article/details/77967964


RECOMMEND

推荐阅读

京东技术系统的一些备战日常优化(2)

Puppet监控速查手册:问题/原因→解决方案

京东自研图计算系统 | JoyGraph

基于Swagger的前后端协同开发解决方案-SMock


京东技术

---关注技术的公众号

长按识别二维码关注

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存